<?php

require_once dirname(__FILE__).'/XMLConfigParser.php';

/**
 * Класс RL_OMLConfigParser парсит конфиги объектного xml
 *
 * Объектный XML это XML специальной структуры.
 *
 * Структура объектного XML
 *
 * Документы объектного XML могут содержать следующие структуры:
 *
 * 1. Объекты
 *
 * <object [type="TypeName"]>
 *	<prop_name_1>{object xml}</prop_name_1>
 *	<prop_name_2>{object xml}</prop_name_2>
 *	...
 *	<prop_name_n>{object xml}</prop_name_n>
 * </object>
 *
 * Аттрибут type указывает класс на основе которого создавать объект.  Если тип не
 * указан, используется тип по умолчанию 'StdClass'.
 *
 * Непосредственно вложенные теги указывают имена свойств объекта, а их содержимое -- значения свойств.
 *
 * 2. Списки:
 * <list [type="TypeName"]>
 *	{object xml}
 *	{object xml}
 *	...
 *	{object xml}
 * </object>
 *
 * Аттрибут type указывает класс на основе которого создавать список.  Если тип не
 * указан, используется тип по умолчанию 'ArrayObject'.
 *
 * Непосредственно вложенные теги указывают значения элементов списка.
 *
 * HINT: Hash-списки, это, скорее всего, RL_AttributeList.
 *
 *
 * Требования к классам
 *
 * В классах объектов и списков, загружаемых из OML должен быть задан конструктор по умолчанию (без параметров).
 * Могут быть определены setters для атрибутов вида 'set{prop_name}'. В этом случае аттрибуты
 * устанавливаются при помощи этих сеттеров.
 *
 * @package RL
 * @author Lucky <lucky@jetstyle.ru>
 * @version $Id: OmlConfigParser.php 62 2007-09-05 08:27:21Z lucky $
 */
class RL_OMLConfigParser extends RL_XMLConfigParser
{

	/**
	 * default_object_type имя класса для объектов по умолчанию
	 *
	 * @var string
	 * @access public
	 */
	public $default_object_type = 'StdClass';
	/**
	 * default_list_type имя класса для списков по умолчанию
	 *
	 * @var string
	 * @access public
	 */
	public $default_list_type = 'ArrayObject';

	/**
	 * type_aliases hash-список псевдонимов для типов и их реальных классов
	 *
	 * @var array
	 * @access public
	 */
	public $type_aliases = array();

	/**
	 * constructor функция для создания объектов
	 *
	 * @var callable
	 * @access public
	 */
	public $constructor = NULL;

	//!an accessor
	/**
	 * parseString
	 *
	 * @param mixed $ctx <{}>
	 * @param mixed $xmlstr <{}>
	 * @access public
	 * @return void
	 */
	public function parseString($xmlstr)
	{
		$node = parent::parseString($xmlstr);

		return $this->parseXMLNode($node);
	}

	//!a manipulator
	/**
	 * addTypeAlias добавить псевдоним для типа
	 *
	 * Псевдонимы помогают абстрагировать типы данных OML от реальных названий классов,
	 * сократить писанину внутри XML, и запутать всех на свете.
	 *
	 * $parser->addTypeAlias('Table', 'RL_DBAL_Mysql_Table');
	 *
	 * <?xml version="1.0"?>
	 * <object type="Table">...</object>
	 *
	 * @param mixed $alias псевдоним типа
	 * @param mixed $real_name настоящее название класса
	 * @access public
	 * @return void
	 */
	public function addTypeAlias ($alias, $real_name)
	{
		$this->type_aliases[$alias] = $real_name;
		return $this;
	} // end of function addTypeAlias




	/// private:
	/// дальше не трогаем )


	//!an accessor
	/**
	 * getType Вернуть настоящее имя типа вместо псевдонима
	 *
	 * @param mixed $type_name имя типа
	 * @access private
	 * @return string
	 */
	private function getType ($type_name)
	{
		$class = isset($this->type_aliases[$type_name]) ? $this->type_aliases[$type_name] : $type_name;
		return $class;
	} // end of function getType

	//!an accessor
	/**
	 * parseXMLNodeObject парсить узел XML как объект
	 *
	 * @param mixed $node узел XML
	 * @access private
	 * @return mixed объект StdClass или подобный
	 */
	private function parseXMLNodeObject($node)
	{
		// узнаем тип объекта
		$type = isset($node['type']) ? (string)$node['type'] 
		                             : $this->default_object_type;
		$real_type = $this->getType($type);

		// создадим объект
		$o = $this->createObject($real_type);

		// заполним атрибуты объекта из атрибутов узла
		foreach ($node->attributes() as $k => $v) { // для атрибута key="value"
			// $k - имя атрибута, $v - значение
			$val = (string)$v;

			// пропустим специальные атрибуты
			if ($val === 'type') continue; 

			// установим атрибут в объекте
			// $k - имя атрибута, $val - значение (как строка или структура PHP)
			$setter = 'set'.$k;
			if (method_exists($o, $setter)) {
				$o->$setter($val);
			} else {
				$o->$k = $val;
			}
		}
		// заполним атрибуты объекта из дочерних узлов
		foreach ($node->children() as $k => $v) { // для каждого узла, задающего атрибут
			// $k - имя атрибута, $v - узел XML (значение атрибута находится внутри узла)

			// FIXME lucky: workaround http://bugs.php.net/bug.php?id=41867
			//
			// $attr = $v->children();
			// $val = (count($attr)) ? $this->parseXMLNode($attr) : (string) $attr;
			$val = (string)$v;
			foreach ($v as $value) { // реально один цикл
				$val = $this->parseXMLNode($value);
				break;
			}

			// установим атрибут в объекте
			// $k - имя атрибута, $val - значение (как строка или структура PHP)
			$setter = 'set'.$k;
			if (method_exists($o, $setter)) {
				$o->$setter($val);
			} else {
				$o->$k = $val;
			}
		}

		return $o;
	}

	//!a function
	/**
	 * listAppend добавить элемент в конец списка.
	 *
	 * В зависимости от типа аргумента вызывает метод добавления
	 *
	 * @param mixed $self что-то типа "Список"
	 * @param mixed $item что добавить
	 * @access private
	 * @return $self
	 */
	private function listAppend (&$self, $item)
	{
		if (is_array($self)) $self[] = $item;
		else $self->append($item);
		return $self;
	} // end of function listAppend
	//!an accessor
	/**
	 * parseXMLNodeList парсить узел XML как список
	 *
	 * @param mixed $node узел XML
	 * @access private
	 * @return mixed объект списка ArrayObject или подобный
	 */
	private function parseXMLNodeList($node)
	{
		// узнаем класс объекта
		$type = isset($node['type']) ? (string)$node['type'] : $this->default_list_type;
		$real_type = $this->getType($type);

		// создадим список
		$o = $this->createObject($real_type);

		// заполним список
		if (0 === count($node)) { // если сказано что строка это список, почему бы и нет?
			$this->listAppend($o, (string)$node);
		} else {
			foreach ($node as $v) { // для каждого элемента списка
				$val = $this->parseXMLNode($v);
				$this->listAppend($o, $val);
			}
		}

		return $o;
	}

	//!an accessor
	/**
	 * parseXMLNode парсить узел XML
	 *
	 * @param mixed $node узел XML
	 * @access private
	 * @return mixed структура объектов
	 */
	private function parseXMLNode($node)
	{
		$node_name = strtolower($node->getName());

		switch ($node_name)
		{
		case 'object':
			$o = $this->parseXMLNodeObject($node);
			break;
		case 'list':
			$o = $this->parseXMLNodeList($node);
			break;
		default:
			throw new Exception ("Parse error: unexpected XML node '${node_name}'");
		}

		return $o;
	}

	//!an accessor
	/**
	 * createObject создать объект заданного типа
	 *
	 * FIXME lucky: поиметь тут возможность интеграции с системой типов
	 *
	 * @param mixed $type тип объекта
	 * @access private
	 * @return mixed объект
	 */
	private function createObject ($type)
	{
		if (isset($this->constructor)) {
			$o = call_user_func($this->constructor, $type);
		} elseif (class_exists($type) || $this->useClass($type)) {
			// нормальный класс
			$o = new $type();
		} elseif (is_callable($type)) {
			// функция-конструктор должна вернуть объект
			$o = $type();
		} elseif (strtolower($type) === 'array') {
			// массив встроенного типа Array
			$o = array();
		} else {
			// облом
			$o = NULL;
			throw new Exception("Type constructor '{$type}' not found");
		}

		return $o;
	} // end of function createObject

	public function useClass ($class)
	{
		if (isset ($this->ctx)) {
			return $this->ctx->rh->useClass($class);
		} else {
			return False;
		}
	} // end of function useClass

}

